package com.yuma.tingshu.album.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yuma.tingshu.album.cache.YuMaCache;
import com.yuma.tingshu.album.mapper.AlbumInfoMapper;
import com.yuma.tingshu.album.mapper.TrackInfoMapper;
import com.yuma.tingshu.album.mapper.TrackStatMapper;
import com.yuma.tingshu.album.service.TrackInfoService;
import com.yuma.tingshu.album.service.VodService;
import com.yuma.tingshu.common.constant.RedisConstant;
import com.yuma.tingshu.common.constant.SystemConstant;
import com.yuma.tingshu.common.execption.YumaException;
import com.yuma.tingshu.common.result.Result;
import com.yuma.tingshu.common.result.ResultCodeEnum;
import com.yuma.tingshu.common.util.AuthContextHolder;
import com.yuma.tingshu.model.album.AlbumInfo;
import com.yuma.tingshu.model.album.TrackInfo;
import com.yuma.tingshu.model.album.TrackStat;
import com.yuma.tingshu.model.user.UserInfo;
import com.yuma.tingshu.model.user.UserPaidTrack;
import com.yuma.tingshu.query.album.TrackInfoQuery;
import com.yuma.tingshu.user.client.UserInfoFeignClient;
import com.yuma.tingshu.vo.album.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class TrackInfoServiceImpl extends ServiceImpl<TrackInfoMapper, TrackInfo> implements TrackInfoService {

    @Autowired
    private TrackInfoMapper trackInfoMapper;

    @Autowired
    private VodService vodService;

    @Autowired
    private AlbumInfoMapper albumInfoMapper;

    @Autowired
    private TrackStatMapper trackStatMapper;

    @Autowired
    private UserInfoFeignClient userInfoFeignClient;

    @Resource(name = "trackBloomFilter")
    private RBloomFilter trackBloomFilter;

    //@Override
    public Page<AlbumTrackListVo> findAlbumTrackPage(Long albumId, Integer page, Integer limit) {
        // 1.判断专辑id是否是空
        if (albumId == null) return null;

        // 1.根据albumId获取专辑信息
        AlbumInfo albumInfo = this.albumInfoMapper.selectOne(new LambdaQueryWrapper<AlbumInfo>().eq(AlbumInfo::getId, albumId));
        if (albumInfo == null) return null;

        /*albumInfo.getPayType()//专辑类型
		albumInfo.getTracksForFree()//免费声音条数
		albumInfo.getUserId()*/
        // 2.获取该专辑的声音列表
        Page<AlbumTrackListVo> allTrackList = this.trackInfoMapper.findAlbumTrackPage(albumId, new Page<AlbumTrackListVo>(page, limit));

        //3.情况1：免费专辑：直接返回声音分页列表，所有声音都不显示付费标记
        if (SystemConstant.ALBUM_PAY_TYPE_FREE.equals(albumInfo.getPayType())) {
            return allTrackList;
        }

        //4.情况2：非免费专辑，没有登录：最前面若干声音免费（比如5集，也可能0集，查看指定字段的值）
        Long userId = AuthContextHolder.getUserId();
        if (userId == null) { //&& !SystemConstant.ALBUM_PAY_TYPE_FREE.equals(albumInfo.getPayType())
            List<AlbumTrackListVo> needPayTracks = allTrackList.getRecords();
            needPayTracks.stream()
                    .filter(track -> track.getOrderNum() > albumInfo.getTracksForFree()) //获取前n个不收费之外的声音
                    .forEach(track -> track.setIsShowPaidMark(true)); //前n个不收费之外需要收费
            return allTrackList;
        }

        //5.情况3：VIP免费专辑,用户已经登录。查询用户是VIP，并且VIP没有过期--->所有声音都不显示付费标记
        Result<UserInfo> userInfoResult = this.userInfoFeignClient.getUserById(userId);
        UserInfo userInfo = userInfoResult.getData();
        if (userInfo == null) {
            return null;
        }
        if (SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(albumInfo.getPayType())) { //VIP免费专辑
            if (userInfo.getIsVip() == 1) { //用户是VIP
                if (userInfo.getVipExpireTime().after(new Date())) { //VIP没有过期
                    return allTrackList;
                }
            }
        }

        //6.情况4：其他：
        // 1.付费专辑，用户已经登录；
        // 2.VIP免费专辑，用户不是VIP
        // 3.VIP免费专辑，用户是VIP，但是已经过期
        // 这就是else条件，暂时不写了
        boolean flag1 = SystemConstant.ALBUM_PAY_TYPE_REQUIRE.equals(albumInfo.getPayType()) && userId != null;
        boolean flag2 = SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(albumInfo.getPayType()) && userInfo.getIsVip() != 1;
        boolean flag3 = SystemConstant.ALBUM_PAY_TYPE_VIPFREE.equals(albumInfo.getPayType())
                && userInfo.getIsVip() == 1
                && userInfo.getVipExpireTime().before(new Date());
        if (flag1 || flag2 || flag3) {

        }
        // 用户是否购买了该专辑：购买：所有的声音都不显示付费标记
        Result<Boolean> paidAlbumResult = this.userInfoFeignClient.isPaidAlbum(albumId);
        Boolean paid = paidAlbumResult.getData();
        if (paid) { // 确定专辑被购买：所有的声音都不显示付费标记
            return allTrackList;
        } else { // 专辑没有被购买--->用户是否购买了专辑内若干声音
            Result<List<Long>> paidTrackIdListResult = this.userInfoFeignClient.getPaidTracksIdByAlbum(albumId);
            List<Long> paidTrackIdlist = paidTrackIdListResult.getData(); //[6,10,20]
            // 没有购买若干声音：最前面若干声音免费
            if (CollectionUtils.isEmpty(paidTrackIdlist)) {
                List<AlbumTrackListVo> albumTrackListVoList = allTrackList.getRecords();
                albumTrackListVoList.stream()
                        .filter(track -> track.getOrderNum() > albumInfo.getTracksForFree()) //获取前n个不收费之外的声音
                        .forEach(track -> track.setIsShowPaidMark(true)); //前n个不收费之外的需要收费
                return allTrackList;
            } else {
                // 购买了若干声音：前面免费的若干，用户购买的声音也免费
                List<AlbumTrackListVo> albumTrackListVoList = allTrackList.getRecords();
                albumTrackListVoList.stream()
                        .filter(track -> track.getOrderNum() > albumInfo.getTracksForFree()) //获取前n个不收费之外的声音
                        .filter(track -> !paidTrackIdlist.contains(track.getTrackId())) //[5,10,20]
                        .forEach(track -> track.setIsShowPaidMark(true)); //前n个不收费之外需要收费
                return allTrackList;
            }
        }
    }

    @Transactional
    @Override
    public void removeTrackInfo(Long trackId) {
        // 1.声音状态删除 四条
        trackStatMapper.delete(new LambdaQueryWrapper<TrackStat>().eq(TrackStat::getTrackId, trackId));
        // 2.专辑表更新声音数量 -1
        TrackInfo trackInfo = trackInfoMapper.selectById(trackId);
        AlbumInfo albumInfo = albumInfoMapper.selectOne(new LambdaQueryWrapper<AlbumInfo>()
                .eq(AlbumInfo::getId, trackInfo.getAlbumId()));
        Integer trackCount = albumInfo.getIncludeTrackCount();
        albumInfo.setIncludeTrackCount(trackCount != 0 ? trackCount - 1 : 0);
        albumInfoMapper.updateById(albumInfo);
        // 3.声音表删除 一条
        trackInfoMapper.deleteById(trackId);
        // 4.vod云点播上删除 medisFileId
        vodService.deleteMedia(trackInfo.getMediaFileId());
    }

    @Override
    public void updateTrackInfo(Long trackId, TrackInfo trackInfo) {
        // 再查询一次媒体对象，通过返回的上传媒体id重新查询媒体信息以覆盖掉之前的媒体信息
        TrackMediaInfoVo medisInfo = vodService.getMediaInfo(trackInfo.getMediaFileId());
        trackInfo.setMediaDuration(BigDecimal.valueOf(medisInfo.getDuration()));
        trackInfo.setMediaUrl(medisInfo.getMediaUrl());
        trackInfo.setMediaSize(medisInfo.getSize());
        trackInfo.setMediaType(medisInfo.getType());
        trackInfoMapper.updateById(trackInfo);
    }

    @Override
    public Page<TrackListVo> findUserTrackPage(Integer page, Integer limit, TrackInfoQuery query) {
        return this.trackInfoMapper.findUserTrackPage(new Page<TrackListVo>(page, limit), query);
    }

    @Override
    public void saveTrackInfo(TrackInfoVo trackInfoVo) {
        TrackInfo trackInfo = new TrackInfo();
        BeanUtils.copyProperties(trackInfoVo, trackInfo);
        // 设置用户id
        Long userId = AuthContextHolder.getUserId();
        trackInfo.setUserId(userId == null ? 1L : userId);

        // 查询同一个专辑下最大的声音序号，进行递增
        TrackInfo one = this.getOne(new LambdaQueryWrapper<TrackInfo>()
                .eq(TrackInfo::getAlbumId, trackInfoVo.getAlbumId())
                .select(TrackInfo::getOrderNum)
                .orderByDesc(TrackInfo::getOrderNum)
                .last("limit 1"));
        trackInfo.setOrderNum(one == null ? 1 : one.getOrderNum() + 1);

        // 1.声音表： 根据fileId查询媒体信息
        TrackMediaInfoVo mediaInfo = vodService.getMediaInfo(trackInfoVo.getMediaFileId());
        trackInfo.setMediaDuration(new BigDecimal(mediaInfo.getDuration()));
        trackInfo.setMediaUrl(mediaInfo.getMediaUrl());
        trackInfo.setMediaType(mediaInfo.getType());
        trackInfo.setMediaSize(mediaInfo.getSize());
        // 来源，用户自创
        trackInfo.setSource(SystemConstant.TRACK_SOURCE_USER);
        // 审核状态通过
        trackInfo.setStatus(SystemConstant.TRACK_STATUS_PASS);

        this.save(trackInfo);

        Long trackId = trackInfo.getId();

        // 新增声音添加到布隆过滤器
        this.trackBloomFilter.add(RedisConstant.TRACK_INFO_PREFIX + trackId);

        // 2.声音统计表
        initTrackStat(trackId, SystemConstant.TRACK_STAT_PLAY);
        initTrackStat(trackId, SystemConstant.TRACK_STAT_COLLECT);
        initTrackStat(trackId, SystemConstant.TRACK_STAT_PRAISE);
        initTrackStat(trackId, SystemConstant.TRACK_STAT_COMMENT);

        // 3.更新专辑中的包含声音数量
        AlbumInfo albumInfo = this.albumInfoMapper.selectById(trackInfoVo.getAlbumId());
        albumInfo.setIncludeTrackCount(albumInfo.getIncludeTrackCount() + 1);
        this.albumInfoMapper.updateById(albumInfo);
    }

    private void initTrackStat(Long trackId, String statType) {
        TrackStat trackStat = new TrackStat();
        trackStat.setTrackId(trackId);
        trackStat.setStatType(statType);
        trackStatMapper.insert(trackStat);
    }

    @Override
    public TrackStatVo getBytrackId(Long trackId) {
        return trackInfoMapper.getTrackStatVo(trackId);
    }

    @YuMaCache(
            prefix = RedisConstant.TRACK_INFO_PREFIX,
            bloomFilter = RedisConstant.TRACK_BLOOM_FILTER,
            lock = RedisConstant.TRACK_LOCK_PREFIX,
            randomBounds = 1000,
            timeout = RedisConstant.TRACK_TIMEOUT
    )
    @Override
    public TrackInfo getTrackInfoById(Long trackId) {
        if(trackId != null){
            return this.trackInfoMapper.selectById(trackId);
        }else {
            return null;
        }
        /*//1.先从缓存中拿取声音
        TrackInfo trackInfo = (TrackInfo) redisTemplate.opsForValue().get(RedisConstant.TRACK_INFO_PREFIX + trackId);
        //2.判空
        if (trackInfo != null) {
            return Result.ok(trackInfo);
        }
        //3.防止缓存穿透，使用布隆过滤器
        boolean contains = bloomFilter.contains(RedisConstant.TRACK_INFO_PREFIX + trackId);
        if (!contains) {
            return null;
        }

        //2.防止缓存击穿，对热点数据加分布式锁
        RLock fairLock = redissonClient.getFairLock(RedisConstant.TRACK_LOCK_PREFIX + trackId);
        fairLock.lock();
        try {
            trackInfo = (TrackInfo) redisTemplate.opsForValue().get(RedisConstant.TRACK_INFO_PREFIX + trackId);
            //2.判空
            if (trackInfo != null) {
                return Result.ok(trackInfo);
            }

            trackInfo = this.trackInfoService.getById(trackId);

            //1.防止缓存雪崩
            long timeout = RedisConstant.TRACK_TIMEOUT + ThreadLocalRandom.current().nextLong(1000);
            redisTemplate.opsForValue().set(RedisConstant.TRACK_INFO_PREFIX + trackId, trackInfo, timeout, TimeUnit.SECONDS);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            fairLock.unlock();
        }
        return Result.ok(trackInfo);*/

        /*//1.先从缓存中拿取声音
        TrackInfo trackInfo = (TrackInfo) redisTemplate.opsForValue().get(RedisConstant.TRACK_INFO_PREFIX + trackId);
        //2.判空
        if (trackInfo != null) {
            return Result.ok(trackInfo);
        }
        //2.防止缓存击穿，对热点数据加分布式锁
        RLock fairLock = redissonClient.getFairLock(RedisConstant.TRACK_LOCK_PREFIX + trackId);
        fairLock.lock();
        try {
            trackInfo = (TrackInfo) redisTemplate.opsForValue().get(RedisConstant.TRACK_INFO_PREFIX + trackId);
            //2.判空
            if (trackInfo != null) {
                return Result.ok(trackInfo);
            }

            trackInfo = this.trackInfoService.getById(trackId);

            //1.防止缓存雪崩
            long timeout = RedisConstant.TRACK_TIMEOUT + ThreadLocalRandom.current().nextLong(1000);
            redisTemplate.opsForValue().set(RedisConstant.TRACK_INFO_PREFIX + trackId, trackInfo, timeout, TimeUnit.SECONDS);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            fairLock.unlock();
        }
        return Result.ok(trackInfo);*/

        /*//1.先从缓存中拿取声音
        TrackInfo trackInfo = (TrackInfo) redisTemplate.opsForValue().get(RedisConstant.TRACK_INFO_PREFIX + trackId);
        //2.判空
        if (trackInfo != null) {
            return Result.ok(trackInfo);
        }
        //2.防止缓存击穿，对热点数据进行手动加锁
        *//*setIfAbsent(key, value) 等价于 Redis 的 SET key value NX 命令 —— 只有当 key 不存在时，才设置值。常用于实现“分布式锁*//*
        Boolean result = redisTemplate.opsForValue()
                .setIfAbsent(RedisConstant.TRACK_LOCK_PREFIX + trackId, "1", 30, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(result)) {
            Thread a = new Thread(() -> {
                while (true) {
                    redisTemplate.expire(RedisConstant.TRACK_LOCK_PREFIX + trackId, 30, TimeUnit.SECONDS);
                    try {
                        ThreadUtil.sleep(10 * 1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            a.start();

            trackInfo = this.trackInfoService.getById(trackId);

            //1.防止缓存雪崩
            long timeout = RedisConstant.TRACK_TIMEOUT + ThreadLocalRandom.current().nextLong(1000);
            redisTemplate.opsForValue().set(RedisConstant.TRACK_INFO_PREFIX + trackId, trackInfo, timeout, TimeUnit.SECONDS);
            redisTemplate.delete(RedisConstant.TRACK_LOCK_PREFIX + trackId);
            a.interrupt();
        } else {
            trackInfo = (TrackInfo) redisTemplate.opsForValue().get(RedisConstant.TRACK_INFO_PREFIX + trackId);
            //2.判空
            if (trackInfo != null) {
                return Result.ok(trackInfo);
            }
        }
        return Result.ok(trackInfo);
        */
    }

    @Override
    public List<TrackOrderVo> findUserTrackPaidList(Long trackId) {
        // 1.判空
        if (trackId == null) {
            throw new YumaException(ResultCodeEnum.ARGUMENT_VALID_ERROR);
        }
        // 2.创建List<TrackOrderVo>对象
        List<TrackOrderVo> trackOrderVoList = new ArrayList<>();
        // 3.根据trackId查询TrackInfo,获取albumId
        TrackInfo trackInfo = this.trackInfoMapper.selectById(trackId);
        if (trackInfo == null) {
            throw new YumaException(ResultCodeEnum.DATA_ERROR);
        }
        // 4.远程调用user模块的findUserTrackPaidList(albumId) // 获取用户已购的声音集合
        Result<List<Long>> paidTracksIdByAlbum = userInfoFeignClient.getPaidTracksIdByAlbum(trackInfo.getAlbumId());
        Assert.notNull(paidTracksIdByAlbum, "声音订购模式:查询用户购买声音失败");
        List<Long> paidTracksId = paidTracksIdByAlbum.getData();
        Assert.notNull(paidTracksId, "声音订购模式:查询用户购买声音失败");
        // 5.根据albumId和orderNum查询当前专辑，当前声音后的所有声音集合
        List<TrackInfo> beforeCurrentTracks = trackInfoMapper.selectList(new LambdaQueryWrapper<TrackInfo>()
                .eq(TrackInfo::getAlbumId, trackInfo.getAlbumId())
                .ge(TrackInfo::getOrderNum, trackInfo.getOrderNum()));
        // 6. 5.查询结果中过滤 3结果集
        // 7.过滤掉后再判断需要购买的声音集合是否为空，为空直接返回空集合
        List<TrackInfo> needBuyTrackList = beforeCurrentTracks.stream().filter((track) -> {
            if (!CollectionUtils.isEmpty(paidTracksId) && paidTracksId.contains(track.getId())) {
                return false;
            } else {
                return true;
            }
        }).toList();

        if (CollectionUtils.isEmpty(needBuyTrackList)) {
            return Collections.EMPTY_LIST;
        }

        AlbumInfo albumInfo = albumInfoMapper.selectById(trackInfo.getAlbumId());

        // 8.否则不为空根据需要购买的声音数量封装TrackOrderVo对象
        // 8.1获取需要购买的数量
        int count = needBuyTrackList.size();
        if (count >= 1) {
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("本集");
            trackOrderVo.setPrice(albumInfo.getPrice());
            trackOrderVo.setTrackCount(1);
            trackOrderVoList.add(trackOrderVo);
        }
        if (count < 10) {
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("后" + count + "集");
            trackOrderVo.setPrice(albumInfo.getPrice().multiply(BigDecimal.valueOf(count)));
            trackOrderVo.setTrackCount(count);
            trackOrderVoList.add(trackOrderVo);
        }
        if (count >= 10) {
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("后10集");
            trackOrderVo.setPrice(albumInfo.getPrice().multiply(BigDecimal.valueOf(10)));
            trackOrderVo.setTrackCount(10);
            trackOrderVoList.add(trackOrderVo);
        }
        // 大于10集小于等于20集
        if (count > 10 && count <= 20){
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("后" + count + "集");
            trackOrderVo.setPrice(albumInfo.getPrice().multiply(new BigDecimal(count)));
            trackOrderVo.setTrackCount(count);
            trackOrderVoList.add(trackOrderVo);
        }
        // 显示后20集
        if (count > 20){
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("后20集");
            trackOrderVo.setPrice(albumInfo.getPrice().multiply(new BigDecimal(20)));
            trackOrderVo.setTrackCount(20);
            trackOrderVoList.add(trackOrderVo);
        }
        // 大于20集小于等于30集
        if (count > 20 && count <= 30){
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("后" + count + "集");
            trackOrderVo.setPrice(albumInfo.getPrice().multiply(new BigDecimal(count)));
            trackOrderVo.setTrackCount(count);
            trackOrderVoList.add(trackOrderVo);
        }
        // 显示后30集
        if (count > 30){
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("后30集");
            trackOrderVo.setPrice(albumInfo.getPrice().multiply(new BigDecimal(30)));
            trackOrderVo.setTrackCount(30);
            trackOrderVoList.add(trackOrderVo);
        }
        // 大于30小于等于50
        if (count > 30 && count <= 50){
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("后" + count + "集");
            trackOrderVo.setPrice(albumInfo.getPrice().multiply(new BigDecimal(count)));
            trackOrderVo.setTrackCount(count);
            trackOrderVoList.add(trackOrderVo);
        }
        // 大于50
        if (count > 50){
            TrackOrderVo trackOrderVo = new TrackOrderVo();
            trackOrderVo.setName("后50集");
            trackOrderVo.setPrice(albumInfo.getPrice().multiply(new BigDecimal(50)));
            trackOrderVo.setTrackCount(50);
            trackOrderVoList.add(trackOrderVo);
        }
        return trackOrderVoList;
    }

    @Override
    public List<TrackInfo> findTradeTrackList(Long trackId, Integer tradeCount) {
        // 1.根据声trackId查询trackInfo
        TrackInfo trackInfo = this.trackInfoMapper.selectById(trackId);
        // 2.判空
        Assert.notNull(trackInfo, "该声音不存在！");

        // 3.根据专辑id查询用户已经购买过的声音列表远程调用User模块
        Result<List<Long>> paidTracksIdByAlbum = userInfoFeignClient.getPaidTracksIdByAlbum(trackInfo.getAlbumId());
        Assert.notNull(paidTracksIdByAlbum, "声音订购模式:查询用户购买声音失败");
        List<Long> paidTracksId = paidTracksIdByAlbum.getData();
        Assert.notNull(paidTracksId, "声音订购模式:查询用户购买声音失败");

        // 4.从本次要购买的声音列表中排除掉用户已经购买的声音，根据orderNum 升序查询前trackCount个声音
        LambdaQueryWrapper<TrackInfo> queryWrapper = new LambdaQueryWrapper<TrackInfo>()
                .eq(TrackInfo::getAlbumId, trackInfo.getAlbumId())
                .ge(TrackInfo::getOrderNum, trackInfo.getOrderNum())
                .orderByAsc(TrackInfo::getOrderNum)
                .select(TrackInfo::getId, TrackInfo::getTrackTitle, TrackInfo::getCoverUrl, TrackInfo::getAlbumId)
                .last("limit " + tradeCount);
        // 因为如果查询是空集合 拼接的 not in()，这样sql会报错
        if (!CollectionUtils.isEmpty(paidTracksId)) {
            queryWrapper.notIn(TrackInfo::getId, paidTracksId);
        }
        List<TrackInfo> trackInfos = trackInfoMapper.selectList(queryWrapper);
        // 5.返回结果
        return CollectionUtils.isEmpty(trackInfos) ? Collections.EMPTY_LIST : trackInfos;
    }
}
